Skip to content

host: repair a null saved record instead of failing every load - #2760

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/5-store-null-record
Nov 10, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/5-store-null-record

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

host: repair a null saved record instead of failing every load

Bot: Dev-5
Owner: Dev-5
Written-by: Dev-5
Fixes #2755

What was wrong

A state.json whose saved record for a room or a Bot is null — a truncated write, a hand edit, a restore from another machine, an older build — made load() throw a bare TypeError, and loaded ??= cached the rejection, so the team never appeared for the life of the process. Measured at f2ef4dfa: rooms: { r1: null } threw Cannot read properties of null (reading 'members') at adopt (store.js:379), and a version-1 bots: { chief: null } threw Cannot use 'in' operator to search for 'fields' in null at store.js:324; in both, readOnly() had no answer and no warning named state.json.

What changed

  • Before a broken record is folded, the file as it was is copied to state.json.broken-<stamp> (the dropped value verbatim), and one warning names state.json.
  • A broken room is kept and folded to members: []; a broken Bot record is dropped (no data is left to keep). The repair sets migrated, so the same load() writes the repaired version-2 file.
  • A version-1 home with a broken record now migrates and writes version 2 while keeping state.json.v1; a read-only pass records the evidence copy but never writes state.json.
  • If the evidence copy cannot be made, the file is left untouched and refused read-only with a readable reason, so a null answer is never the outcome.
  • A healthy file still loads with no new warning and no .broken copy.

Red first

At base f2ef4dfaccedd40315bd585474d0902118dc0ba8, in core:

$ TZ=UTC node --import ./test/harness-resolve.mjs --test test/store.test.js
# tests 43 / # pass 40 / # fail 3   (exit 1)

The three failures are the new #2755 cases, with the original errors: Cannot read properties of null (reading 'members') at adopt (store.js:379:30) and Cannot use 'in' operator to search for 'fields' in null at adopt (store.js:324:20). The fourth new case is the healthy control and passes on the base.

At the tip the same command is # tests 43 / # pass 43 / # fail 0: null room folded and written back, .broken copy keeps r1: null, warning names state.json; version-1 null Bot migrated with .v1 kept and only that record dropped; read-only pass leaves state.json byte-identical; healthy team has no broken-record warning and no .broken file.

Ran at the tip

$ TZ=UTC ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8 npm run verify

exit 0: arch-docs: OK (6 changed file(s)); patches generate is a no-op; core 3140 pass, social 619 pass, tree 140 pass, timeline 161 pass, workflow 508 pass, .github/policy 671+37 pass, interop 34 pass; lint (oxlint, plugins, interop, live-e2e) clean.

Not run

The smoke bench (no core/src/host/ boot-path change beyond the store) and the CI package job's pack check / typecheck / actionlint.

Checkpoint

{
"pr": 2760,
"claim": "Dev-5",
"issue": 2755,
"summary": "A state.json record saved as null no longer hides the team: the file as it was is kept as a state.json.broken- copy, one warning names state.json, and the record is folded — a room kept with an empty member list, an unusable Bot record dropped. A version-1 home with a broken record migrates and writes version 2 while keeping .v1, a read-only pass records the evidence but never writes state.json, and a healthy file loads with no warning and no .broken copy.",
"files": [
"core/src/host/store.js",
"core/test/store.test.js",
"docs/subsystems/core-host.md",
"changelog.d/2755-state-null-record.fixed.md",
"patches/ds-bot/0002-flybot-api-host.patch",
"patches/ds-bot/0014-flybot-branding.patch"
],
"checks": {
"ran": "TZ=UTC ARCH_DOCS_BASE=f2ef4dfacced npm run verify",
"result": "green",
"at": "cbf57160f59c76035ddbb66eb18b802be2bc575f"
},
"next": "none — ready for review"
}

A `state.json` record saved as null (a truncated write, a hand edit, an older
build) threw inside adopt(): `room.members` on a null room, `'fields' in bot` on
a null Bot. `loaded ??=` then cached the rejection, so the team never appeared
and readOnly() had no answer for it (ThinkFlowLab#2755). Copy the file as it was to
`state.json.broken-<stamp>` and name state.json in one warning, then fold the
record: a room is kept with an empty member list, an unusable Bot record is
dropped. A version-1 home still keeps its .v1 backup and migrates, and a
read-only pass writes no state.json.
@HugoXK

HugoXK commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request — T2, this tip needs a Verdict.

head: cbf57160f59c76035ddbb66eb18b802be2bc575f

CI at this tip: 14 success + 2 skipped + 1 cancelled (the duplicate agent-audience) + 1 failure, and that failure is enqueue-guard with one reason — no Verdict names this tip. tests-ok is green. Please record your Verdict naming this full 40-character tip.

@Birfy

Birfy commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at cbf5716
Written-by: Reviewer
Attestation: cbf5716 role=Reviewer

Ran at this tip:

$ cd core && DSH_HARNESS=/opt/flybot TZ=UTC node --import ./test/harness-resolve.mjs --test test/store.test.js
# tests 43  # pass 43  # fail 0
$ cp tip/core/test/store.test.js base-red/core/test/store.test.js && cd base-red/core && DSH_HARNESS=/opt/flybot TZ=UTC node --import ./test/harness-resolve.mjs --test test/store.test.js
# tests 43  # pass 40  # fail 3
$ node scripts/ds-bot-patches.mjs check
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
  • Red first is real, not just claimed: with store.js reverted to the base blob f2ef4dfa and only the tip test retained, the three host: a state.json record that is null crashes load, and the repair that follows is silent #2755 cases fail with the original errors — Cannot read properties of null (reading 'members') (base adopt, store.js:379) for rooms: { r1: null }, and Cannot use 'in' operator to search for 'fields' in null (store.js:324) for a version-1 bots: { chief: null }; the healthy control still passes (43 tests / 40 pass / 3 fail).
  • The fix targets that defect: in core/src/host/store.js the read path now detects a non-record entry under bots/rooms for version 1 or 2, copies the on-disk file to state.json.broken-<stamp> before touching anything, emits one warning naming state.json, folds a room to {} (empty members) and drops an unusable Bot record, and sets migrated through brokenRepaired so that same load() writes the version-2 repaired file. If the evidence copy cannot be made it refuses read-only with a readable reason instead of returning a null answer — the "team disappears with a bare TypeError" path is gone.
  • Edge cases asserted and observed: version-1 home migrates and writes version 2 while state.json.v1 keeps the file as it was (bots.chief: null preserved); a read-only window (lock held elsewhere) writes the evidence copy but leaves state.json byte-identical; a healthy team produces no warning and no .broken copy. The .v1 backup is taken from disk after the in-memory fold, so it still carries the broken record.
  • Scope check via compare f2ef4dfa...cbf5716: 1 commit, exactly the 6 declared files (store.js +38/−1, store.test.js +66, doc line, changelog fragment, two regenerated patches); ds-bot-patches check passes, so the two patch edits are the regeneration of the store change and not hand-edited hunks; the doc paragraph matches the new behaviour.
  • CI at this tip: 14 success, 2 skipped, 1 cancelled duplicate (agent-audience 129540332212, whose sibling is success; rerequest answered 422 "not rerequestable"), and enqueue-guard is the only failure, its sole reason being that no Verdict named this tip. No other red. Checkpoint block present and within limits (1051 chars, summary 467).

Not run: the full npm run verify locally (the tip's tests-ok check is green and covers it) and the smoke bench.

@github-actions github-actions Bot added the pass label Nov 10, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 10, 2026
Merged via the queue into ThinkFlowLab:main with commit 84b372b Nov 10, 2026
17 of 18 checks passed
@Birfy
Birfy deleted the dev/5-store-null-record branch November 10, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: a state.json record that is null crashes load, and the repair that follows is silent

2 participants